/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.material3

import android.os.Build
import androidx.compose.material3.internal.Strings
import androidx.compose.material3.internal.formatWithSkeleton
import androidx.compose.material3.internal.getString
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertIsNotFocused
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlinx.coroutines.delay
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@MediumTest
@RunWith(AndroidJUnit4::class)
class DateRangeInputTest {

    @get:Rule val rule = createComposeRule()

    @Test
    fun dateRangeInput() {
        lateinit var state: DateRangePickerState
        lateinit var pickerStartDateHeadline: String
        lateinit var pickerEndDateHeadline: String
        rule.setMaterialContent(lightColorScheme()) {
            pickerStartDateHeadline = getString(string = Strings.DateRangePickerStartHeadline)
            pickerEndDateHeadline = getString(string = Strings.DateRangePickerEndHeadline)
            val monthInUtcMillis = dayInUtcMilliseconds(year = 2019, month = 1, dayOfMonth = 1)
            state =
                rememberDateRangePickerState(
                    initialDisplayedMonthMillis = monthInUtcMillis,
                    initialDisplayMode = DisplayMode.Input,
                )
            DateRangePicker(state = state)
        }

        // Expecting 2 nodes with the text "Start date", and 2 with "End date".
        rule
            .onAllNodesWithText(pickerStartDateHeadline, useUnmergedTree = true)
            .assertCountEquals(2)
        rule.onAllNodesWithText(pickerEndDateHeadline, useUnmergedTree = true).assertCountEquals(2)

        // Enter dates.
        rule.onNodeWithText(pickerStartDateHeadline).performClick().performTextInput("01272019")
        rule.onNodeWithText(pickerEndDateHeadline).performClick().performTextInput("05102020")

        rule.runOnIdle {
            assertThat(state.selectedStartDateMillis)
                .isEqualTo(dayInUtcMilliseconds(year = 2019, month = 1, dayOfMonth = 27))
            assertThat(state.selectedEndDateMillis)
                .isEqualTo(dayInUtcMilliseconds(year = 2020, month = 5, dayOfMonth = 10))
        }

        // Now expecting only one node with "Start date", and one with "End date".
        rule
            .onAllNodesWithText(pickerStartDateHeadline, useUnmergedTree = true)
            .assertCountEquals(1)
        rule.onAllNodesWithText(pickerEndDateHeadline, useUnmergedTree = true).assertCountEquals(1)
        rule.onNodeWithText("Jan 27, 2019", useUnmergedTree = true).assertExists()
        rule.onNodeWithText("May 10, 2020", useUnmergedTree = true).assertExists()
    }

    @Test
    fun dateRangeInputWithInitialDates() {
        lateinit var state: DateRangePickerState
        rule.setMaterialContent(lightColorScheme()) {
            val initialStartDateMillis =
                dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
            val initialEndDateMillis =
                dayInUtcMilliseconds(year = 2020, month = 10, dayOfMonth = 20)
            state =
                rememberDateRangePickerState(
                    initialSelectedStartDateMillis = initialStartDateMillis,
                    initialSelectedEndDateMillis = initialEndDateMillis,
                    initialDisplayMode = DisplayMode.Input,
                )
            DateRangePicker(state = state)
        }

        rule.onNodeWithText("05/11/2010").assertExists()
        rule.onNodeWithText("10/20/2020").assertExists()
        rule.onNodeWithText("May 11, 2010", useUnmergedTree = true).assertExists()
        rule.onNodeWithText("Oct 20, 2020", useUnmergedTree = true).assertExists()
    }

    @Test
    fun dateRangeInput_initialFocusOnInputField() {
        var delayCompleted by mutableStateOf(false)
        rule.setMaterialContent(lightColorScheme()) {
            val initialStartDateMillis =
                dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
            val initialEndDateMillis =
                dayInUtcMilliseconds(year = 2020, month = 10, dayOfMonth = 20)
            DateRangePicker(
                state =
                    rememberDateRangePickerState(
                        initialSelectedStartDateMillis = initialStartDateMillis,
                        initialSelectedEndDateMillis = initialEndDateMillis,
                        initialDisplayMode = DisplayMode.Input,
                    )
            )
            // Update the delayCompleted till after the focus is acquired. Note that we request the
            // focus about 400ms after the picker is shown, but using a higher delay here to reduce
            // flakiness.
            LaunchedEffect(Unit) {
                delay(1000)
                delayCompleted = true
            }
        }
        rule.waitUntil("Waiting for focus", 5_000L) { delayCompleted }
        rule.onNodeWithText("05/11/2010").assertIsFocused()
        rule.onNodeWithText("10/20/2020").assertIsNotFocused()
    }

    @Test
    fun dateRangeInput_noInitialFocusOnInputField() {
        var delayCompleted by mutableStateOf(false)
        rule.setMaterialContent(lightColorScheme()) {
            val initialStartDateMillis =
                dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
            val initialEndDateMillis =
                dayInUtcMilliseconds(year = 2020, month = 10, dayOfMonth = 20)
            DateRangePicker(
                state =
                    rememberDateRangePickerState(
                        initialSelectedStartDateMillis = initialStartDateMillis,
                        initialSelectedEndDateMillis = initialEndDateMillis,
                        initialDisplayMode = DisplayMode.Input,
                    ),
                // Prevent the focus from being requested.
                focusRequester = null,
            )
            // Although a focus request is not made, apply a delay to ensure that the test checks
            // for focus after that delay.
            LaunchedEffect(Unit) {
                delay(1000)
                delayCompleted = true
            }
        }
        rule.waitUntil("Waiting for delay completion", 5_000L) { delayCompleted }
        rule.onNodeWithText("05/11/2010").assertIsNotFocused()
        rule.onNodeWithText("10/20/2020").assertIsNotFocused()
    }

    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
    @Test
    fun dateRangeInputWithInitialDate_alternateLocale() {
        lateinit var state: DateRangePickerState
        rule.setMaterialContent(lightColorScheme()) {
            val initialStartDateMillis =
                dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
            val initialEndDateMillis = dayInUtcMilliseconds(year = 2020, month = 5, dayOfMonth = 20)
            state =
                DateRangePickerState(
                    locale = Locale.forLanguageTag("HE"),
                    initialSelectedStartDateMillis = initialStartDateMillis,
                    initialSelectedEndDateMillis = initialEndDateMillis,
                    initialDisplayMode = DisplayMode.Input,
                )
            DateRangePicker(state = state)
        }

        // For Hebrew Locale, the month precedes the date.
        rule.onNodeWithText("11.05.2010").assertExists()
        rule.onNodeWithText("20.05.2020").assertExists()
        // Setting the Locale at the state would not affect the displayed dates at the headline, and
        // it will still be displayed as "May 11, 2010" with the default locale. To ensure that the
        // entire date picker UI is localized, there is a need to wrap the picker's code in a
        // CompositionLocalProvider with a new Context Configuration, but this test does not cover
        // that.
        rule.onNodeWithText("May 11, 2010", useUnmergedTree = true).assertExists()
        rule.onNodeWithText("May 20, 2020", useUnmergedTree = true).assertExists()
    }

    @Test
    fun inputDateNotAllowed() {
        lateinit var startDateRangeInputLabel: String
        lateinit var endDateRangeInputLabel: String
        lateinit var errorMessage: String
        lateinit var state: DateRangePickerState
        rule.setMaterialContent(lightColorScheme()) {
            startDateRangeInputLabel = getString(string = Strings.DateRangePickerStartHeadline)
            endDateRangeInputLabel = getString(string = Strings.DateRangePickerEndHeadline)
            errorMessage = getString(string = Strings.DateInputInvalidNotAllowed)
            state =
                rememberDateRangePickerState(
                    initialDisplayMode = DisplayMode.Input,
                    selectableDates =
                        object : SelectableDates {
                            // All dates are invalid for the sake of this test.
                            override fun isSelectableDate(utcTimeMillis: Long): Boolean = false
                        },
                )
            DateRangePicker(state = state)
        }

        // Enter dates.
        rule.onNodeWithText(startDateRangeInputLabel).performClick().performTextInput("01272019")
        rule.onNodeWithText(endDateRangeInputLabel).performClick().performTextInput("05102020")

        rule.runOnIdle {
            assertThat(state.selectedStartDateMillis).isNull()
            assertThat(state.selectedEndDateMillis).isNull()
        }
        rule
            .onNodeWithText("01/27/2019")
            .assert(keyIsDefined(SemanticsProperties.Error))
            .assert(expectValue(SemanticsProperties.Error, errorMessage.format("Jan 27, 2019")))
        rule
            .onNodeWithText("05/10/2020")
            .assert(keyIsDefined(SemanticsProperties.Error))
            .assert(expectValue(SemanticsProperties.Error, errorMessage.format("May 10, 2020")))
    }

    @Test
    fun outOfOrderDateRange() {
        lateinit var startDateRangeInputLabel: String
        lateinit var endDateRangeInputLabel: String
        lateinit var errorMessage: String
        lateinit var state: DateRangePickerState
        rule.setMaterialContent(lightColorScheme()) {
            startDateRangeInputLabel = getString(string = Strings.DateRangePickerStartHeadline)
            endDateRangeInputLabel = getString(string = Strings.DateRangePickerEndHeadline)
            errorMessage = getString(string = Strings.DateRangeInputInvalidRangeInput)
            state =
                rememberDateRangePickerState(
                    // Limit the years selection to 2018-2023
                    yearRange = IntRange(2018, 2023),
                    initialDisplayMode = DisplayMode.Input,
                )
            DateRangePicker(state = state)
        }

        // Enter dates where the start date is later than the end date.
        rule.onNodeWithText(startDateRangeInputLabel).performClick().performTextInput("01272020")
        rule.onNodeWithText(endDateRangeInputLabel).performClick().performTextInput("05102019")

        rule.runOnIdle {
            // Expecting the first stored date to still be valid, and the second one to be null.
            assertThat(state.selectedStartDateMillis).isNotNull()
            assertThat(state.selectedEndDateMillis).isNull()
        }
        rule
            .onNodeWithText("05/10/2019", useUnmergedTree = true)
            .assert(keyIsDefined(SemanticsProperties.Error))
            .assert(expectValue(SemanticsProperties.Error, errorMessage))
    }

    @Test
    fun switchToDateRangePicker() {
        lateinit var switchToPickerDescription: String
        lateinit var startDateRangeInputLabel: String
        lateinit var endDateRangeInputLabel: String
        lateinit var pickerStartDateHeadline: String
        lateinit var pickerEndDateHeadline: String
        rule.setMaterialContent(lightColorScheme()) {
            switchToPickerDescription = getString(string = Strings.DatePickerSwitchToCalendarMode)
            startDateRangeInputLabel = getString(string = Strings.DateRangePickerStartHeadline)
            endDateRangeInputLabel = getString(string = Strings.DateRangePickerEndHeadline)
            pickerStartDateHeadline = getString(string = Strings.DateRangePickerStartHeadline)
            pickerEndDateHeadline = getString(string = Strings.DateRangePickerEndHeadline)
            DateRangePicker(
                state = rememberDateRangePickerState(initialDisplayMode = DisplayMode.Input)
            )
        }

        // Click to switch to DateRangePicker.
        rule.onNodeWithContentDescription(label = switchToPickerDescription).performClick()

        rule.waitForIdle()
        rule.onNodeWithText(pickerStartDateHeadline, useUnmergedTree = true).assertIsDisplayed()
        rule.onNodeWithText(pickerEndDateHeadline, useUnmergedTree = true).assertIsDisplayed()
        rule.onNodeWithText(startDateRangeInputLabel).assertDoesNotExist()
        rule.onNodeWithText(endDateRangeInputLabel).assertDoesNotExist()
    }

    @Test
    fun defaultSemantics() {
        val startDateMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
        val endDateMillis = dayInUtcMilliseconds(year = 2010, month = 6, dayOfMonth = 12)
        lateinit var pickerStartDateHeadline: String
        lateinit var pickerEndDateHeadline: String
        rule.setMaterialContent(lightColorScheme()) {
            pickerStartDateHeadline = getString(string = Strings.DateRangePickerStartHeadline)
            pickerEndDateHeadline = getString(string = Strings.DateRangePickerEndHeadline)
            DateRangePicker(
                state =
                    rememberDateRangePickerState(
                        initialSelectedStartDateMillis = startDateMillis,
                        initialSelectedEndDateMillis = endDateMillis,
                        initialDisplayMode = DisplayMode.Input,
                    )
            )
        }
        val cache = mutableMapOf<String, Any>()
        val fullStartDateDescription =
            formatWithSkeleton(
                startDateMillis,
                DatePickerDefaults.YearMonthWeekdayDaySkeleton,
                Locale.US,
                cache,
            )
        val fullEndDateDescription =
            formatWithSkeleton(
                endDateMillis,
                DatePickerDefaults.YearMonthWeekdayDaySkeleton,
                Locale.US,
                cache,
            )

        val startHeadlineDescription = "$pickerStartDateHeadline: $fullStartDateDescription"
        val endHeadlineDescription = "$pickerEndDateHeadline: $fullEndDateDescription"
        rule
            .onNodeWithContentDescription("$startHeadlineDescription, $endHeadlineDescription")
            .assertExists()
    }

    // Returns the given date's day as milliseconds from epoch. The returned value is for the day's
    // start on midnight.
    private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long {
        val firstDayCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
        firstDayCalendar.clear()
        firstDayCalendar[Calendar.YEAR] = year
        firstDayCalendar[Calendar.MONTH] = month - 1
        firstDayCalendar[Calendar.DAY_OF_MONTH] = dayOfMonth
        return firstDayCalendar.timeInMillis
    }
}
